<?php

/*
 * Copyright 2010 Mark Watkinson
 * 
 * This file is part of Luminous.
 * 
 * Luminous is free software: you can redistribute it and/or
 * modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Luminous is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Luminous.  If not, see <http://www.gnu.org/licenses/>.
 * 
 */

/**
 * \file luminous_formatter.class.php
 * \brief Formatting logic -- converts Luminous output into displayable formats
 */ 

/** 
 * \brief Abstract class to convert Luminous output into a universal format.
 * 
 * Abstract base class to implement an output formatter. A formatter
 * will convert Luminous's tags into some kind of output (e.g. HTML), by 
 * overriding the method Format().
 */ 
abstract class LuminousFormatter
{
  /// Number of chars to wrap at
  public $wrap_length = 120;
  /// Don't use this yet.
  public $language_specific_tags = false;  
  /**
   * Tab width, in spaces. If this is -1 or 0, tabs will not be converted. This
   * is not recommended as browsers may render tabs as different widths which 
   * will break the wrapping.
   */ 
  public $tab_width = 2;
  
  /// Whether or not to add line numbering
  public $line_numbers = true;
  
  /// sets whether or not to link URIs.
  public $link = true;
  
  
  
  
  /**
   * The main method for interacting with formatter objects. 
   * \param src the input string, which is of the form output by an instance of
   * Luminous.
   * \return The input string reformatted to some other specification.
   */ 
  public abstract function Format($src);
  
  protected abstract function Linkify(&$src);
  
  protected static function GetIndenter($width)
  {
    switch($width)
    {
      case 1: return " ";
      case 2: return "  ";
      case 3: return "   ";
      case 4: return "    ";
      case 5: return "     ";
      case 6: return "      ";
      case 7: return "       ";
      case 8: return "        ";
      case 9: return "         ";
      case 10: return "          ";
    }
    $indent = "          ";
    for ($i=10; $i<$width; $i++)
      $indent .= " ";
    return $indent;
    
  }
  
  /**
   * \internal
   * Handles line wrapping. 
   * \param line the line which needs to be broken. This is a reference, which 
   * will be operated upon. After calling, $line will have appropriate line 
   * breaks to wrap to the given width, and will contain at least one line break
   * at the end.
   * \param wrap_length the width to wrap to. 
   * 
   * \return the number of lines it was broken up into (1 obviously means no 
   *    wrapping occurred.).
   * 
   * \todo wrap to indent? or not? hm.
   * 
   */ 
  protected static function WrapLine(&$line, $wrap_length)
  {
    // The vast majority of lines will not need wrapping so it pays to 
    // check this first.
    if ($wrap_length == -1 || !isset($line[$wrap_length])
      || strlen(strip_tags($line)) < $wrap_length)
    {
      $line .= "\n";
      return 1;
    }
      
    $line_split = preg_split('/((?:<.*?>)|(?:&.*?;)|[ \t]+)/',
      $line, -1,   PREG_SPLIT_NO_EMPTY|PREG_SPLIT_DELIM_CAPTURE);
    
    
    $strlen = 0;
    $line_cpy = "";
    $num_lines = 1;
    
    $num_open = 0;
    foreach($line_split as &$l)
    {
      $l0 = $l[0];
      if ($l0 === '<')
      {        
        $line_cpy .= $l;
        continue;
      }
      
      $s = strlen($l);
      
      if($l0 === '&') //html entity codes only count as 1 char.
      {
        if(++$strlen > $wrap_length)
        {
          $strlen = 1;
          $line_cpy .= "\n";
          $num_lines++;
        }
        $line_cpy .= $l;       
        
        continue;
      }
      if ($s+$strlen <= $wrap_length)
      {
        $line_cpy .= $l;
        $strlen += $s;
        continue;        
      }
      
      if ($s <= $wrap_length)
      {
        $line_cpy .= "\n" . $l;
        $num_lines++;
        $strlen = $s;
        continue;
      }
      // at this point, the line needs wrapping.
      
      // bump us up to the next line      
      $diff = $wrap_length-$strlen;      
      
      $line_cpy .= substr($l, 0, $diff) . "\n";   
      $l_ = substr($l, $diff);
      // now start copying.
      $strlen = 0;
      // this would probably be marginally faster if it did its own arithmetic
      // instead of calling strlen
      
      while (strlen($l_) > 0)
      {
        $strl = strlen($l_);
        $num_lines++;
        
        if ($strl > $wrap_length)
        {
          $line_cpy .= substr($l_, 0, $wrap_length) . "\n";
          $l_ = substr($l_, $wrap_length);
        }
        else
        {
          $line_cpy .= $l_;
          $strlen = $strl;          
          break;
        }
      }
    }
    $line = $line_cpy . "\n";

    return $num_lines;
  }
}

/**
 * \brief HTML Formatter.
 * Converts Luminous code to an HTML widget.
 * 
 * \todo test properly with IE
 */ 
class LuminousFormatterHTML extends LuminousFormatter
{   
  public $height = 0;
  
  public static 
  function GetOpenTags($line, &$open_tags, &$num_open)
  {
    $regex = '%<(/)?(.*?)>%';
    $matches = array();
    preg_match_all($regex, $line, $matches, PREG_SET_ORDER);
//     echo "M:";
//     print_r($matches);
//     echo "/M:";
    foreach($matches as $m)
    {
      if ($m[1] === '/')
      {
        if ($num_open > 0)
        {
          array_pop($open_tags);
          $num_open--;
        }
      }
      else
      {
        $open_tags[] = $m[2];
        $num_open++;
      }
    }
  }
  
  private function Format_NoLineNumbers($src)
  {
    $lines = '';
    $lines_a = explode("\n", $src);
    $lines_untagged = explode("\n", strip_tags($lines));

    foreach($lines_a as $index=>&$line)
    {
      $l = $line;
      $num = $this->WrapLine($l, $this->wrap_length);
      $lines .= $l;
    }
 
    return "<div class='code'><pre class='code'>$lines</pre></div>";
  }
  
  public 
  function Format($src)
  {
    $line_numbers = false;
    
    
    if ($this->tab_width - 1 > 0)
    {
      $tab_rep = "";
      for($i=0; $i<$this->tab_width; $i++)
        $tab_rep .= " ";
      $src = str_replace("\t", $tab_rep, $src);
    }
    if ($this->link)
      $this->Linkify($src);
    
    $lines = "";
    if ($this->line_numbers)
      $lines = $this->Format_LineNumbers($src);
    else
      $lines = $this->Format_NoLineNumbers($src);
    

    $lines = preg_replace('/(?<=<\/)[A-Z_0-9]+(?=>)/', 'span', $lines);
    
    $cb = create_function('$matches', 
      '$m1 = strtolower($matches[1]);
      return "<span class=\'" . $m1 . "\'>";
      ');
      
    $lines = preg_replace_callback('/<([A-Z_0-9]+)>/', $cb, 
      $lines);
    $markup =  $lines ; 
    $h = "" . $this->height;
    $h = trim($h);
    $css = "";
    
    if (strlen($h) && (int)$h > 0)
    {
      $units = !ctype_digit($h[strlen($h)-1]);
      $css = " style=\"max-height:";
      $css .= $h;
      if (!$units)
        $css .= "px";
      $css .= ";\" ";
    }
             
    return "<div class=\"luminous\">" 
      . "<div class=\"code_container\" $css>"
      . "$markup</div></div>";
  }
  
  protected function Linkify(&$src)
  {
    
    if (stripos($src, "http://") === false && stripos($src, "www.") === false)
        return;
    
    $cb = create_function('$matches', ' 
      $uri = (isset($matches[1]) && strlen(trim($matches[1])))? $matches[0] : 
        "http://" . $matches[0];
      
      // we dont want to link if it would cause malformed HTML
      $open_tags = array();
      $close_tags = array();
      preg_match_all("/<(?!\/)([^\s>]*).*?>/", $matches[0], $open_tags, PREG_SET_ORDER);
      preg_match_all("/<\/([^\s>]*).*?>/", $matches[0], $close_tags, PREG_SET_ORDER);
      
      if (count($open_tags) != count($close_tags))
        return $matches[0];
      if (isset($open_tags[0]) && trim($open_tags[0][1]) !== trim($close_tags[0][1]))
        return $matches[0];

      $uri = strip_tags($uri);
      
      return "<a href=\"$uri\" class=\"link\" target=\"_blank\">$matches[0]</a>";
    ');
    
    $chars = "0-9a-zA-Z\$\-_\.+!\*,%";
    // everyone stand back, I know regular expressions
    $src = preg_replace_callback(
      "@(?<![\w])
      (?:(https?://(?:www[0-9]*\.)?) | (?:www\d*\.)   )
      
      # domain and tld
      (?:[$chars]+)+\.[$chars]{2,}
      # we don't include tags at the EOL because these are likely to be 
      # line-enclosing tags.
      # same for quotes.
      (?:[/$chars/?=\#;]+|&amp;|<[^>]+>(?!$)|'(?!\s))*
      @xm",
      $cb, $src);
  }
  
  // this is what we're using now for line numbering.
  private 
  function Format_LineNumbers($src)
  {    

    $lineno = 1;
    $linenos='';
    $lines = '';
    
    $lines_a = explode("\n", $src);
    $lines_untagged = explode("\n", strip_tags($lines));
    
    $id = rand();    
    
    // this seems to run a bit faster if we keep the literals out of
    // the loop.

    $class = "line_number";
    $class_emph = " line_number_emphasised";

    $line_no_tag0 = '<a id="lineno_' . $id . '_';
    $line_no_tag1 = '" class="' . $class;
    $line_no_tag2 = '"><span class="line_number">&nbsp;';
    $line_no_tag3 = "&nbsp;\n</span></a>";
    
    $wrap_line = "<span class='line_number'>&nbsp;|_\n</span>";
    
    $line_tag0 = '<span id="line_' . $id . '_';
    $line_tag1 = '" class="line';
    $class_alt = ' line_alt';
    $line_tag2 = '">';
    $line_tag3 = '</span>';
    
    $line_delta = 3;
    foreach($lines_a as $line)
    {      
      
      $linenos .= $line_no_tag0 . $lineno . $line_no_tag1;
      if ($lineno % 5 === 0)
        $linenos .= $class_emph;
      $linenos .= $line_no_tag2 . $lineno . $line_no_tag3;
      
      
      $num = $this->WrapLine($line, $this->wrap_length);  
      
      for ($i=1; $i<$num; $i++)
        $linenos .= $wrap_line;
      
      
      $lines .= $line_tag0 . $lineno . $line_tag1;
      if ($lineno % 2 === 0)
        $lines .= $class_alt;
      $lines .= $line_tag2 . $line . $line_tag3;
        /*
        
      $lines .= "<span id='line_{$id}_$lineno' class='line";
      if ($lineno % 2 === 0)
        $lines .= ' line_alt';
      $lines .= "'>$line</span>";*/
      
      ++$lineno;
      
    }

    return "<table class='code_container'><tr><td class='line_numbers'>" 
           . "<pre class='line_numbers' style=''>$linenos</pre></td>"
           . "\n<td class='code'><pre class='code'>$lines</pre>"
           . "</td></tr></table>";
  }      
}

